package org.bouncycastle.asn1;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.bouncycastle.util.io.Streams;

/**
 * a general purpose ASN.1 decoder - note: this class differs from the others in
 * that it returns null after it has read the last object in the stream. If an
 * ASN.1 NULL is encountered a DER/BER Null object is returned.
 */
public class ASN1InputStream extends FilterInputStream implements DERTags {
    private final int limit;
    private final boolean lazyEvaluate;

    static int findLimit(InputStream in) {
        if (in instanceof LimitedInputStream) {
            return ((LimitedInputStream) in).getRemaining();
        } else if (in instanceof ByteArrayInputStream) {
            return ((ByteArrayInputStream) in).available();
        }

        return Integer.MAX_VALUE;
    }

    public ASN1InputStream(InputStream is) {
        this(is, findLimit(is));
    }

    /**
     * Create an ASN1InputStream based on the input byte array. The length of
     * DER objects in the stream is automatically limited to the length of the
     * input array.
     * 
     * @param input
     *            array containing ASN.1 encoded data.
     */
    public ASN1InputStream(byte[] input) {
        this(new ByteArrayInputStream(input), input.length);
    }

    /**
     * Create an ASN1InputStream based on the input byte array. The length of
     * DER objects in the stream is automatically limited to the length of the
     * input array.
     * 
     * @param input
     *            array containing ASN.1 encoded data.
     * @param lazyEvaluate
     *            true if parsing inside constructed objects can be delayed.
     */
    public ASN1InputStream(byte[] input, boolean lazyEvaluate) {
        this(new ByteArrayInputStream(input), input.length, lazyEvaluate);
    }

    /**
     * Create an ASN1InputStream where no DER object will be longer than limit.
     * 
     * @param input
     *            stream containing ASN.1 encoded data.
     * @param limit
     *            maximum size of a DER encoded object.
     */
    public ASN1InputStream(InputStream input, int limit) {
        this(input, limit, false);
    }

    /**
     * Create an ASN1InputStream where no DER object will be longer than limit,
     * and constructed objects such as sequences will be parsed lazily.
     * 
     * @param input
     *            stream containing ASN.1 encoded data.
     * @param limit
     *            maximum size of a DER encoded object.
     * @param lazyEvaluate
     *            true if parsing inside constructed objects can be delayed.
     */
    public ASN1InputStream(InputStream input, int limit, boolean lazyEvaluate) {
        super(input);
        this.limit = limit;
        this.lazyEvaluate = lazyEvaluate;
    }

    protected int readLength() throws IOException {
        return readLength(this, limit);
    }

    protected void readFully(byte[] bytes) throws IOException {
        if (Streams.readFully(this, bytes) != bytes.length) {
            throw new EOFException("EOF encountered in middle of object");
        }
    }

    /**
     * build an object given its tag and the number of bytes to construct it
     * from.
     */
    protected DERObject buildObject(int tag, int tagNo, int length)
            throws IOException {
        boolean isConstructed = (tag & CONSTRUCTED) != 0;

        DefiniteLengthInputStream defIn = new DefiniteLengthInputStream(this,
                length);

        if ((tag & APPLICATION) != 0) {
            return new DERApplicationSpecific(isConstructed, tagNo,
                    defIn.toByteArray());
        }

        if ((tag & TAGGED) != 0) {
            return new ASN1StreamParser(defIn).readTaggedObject(isConstructed,
                    tagNo);
        }

        if (isConstructed) {
            // TODO There are other tags that may be constructed (e.g.
            // BIT_STRING)
            switch (tagNo) {
                case OCTET_STRING:
                    //
                    // yes, people actually do this...
                    //
                    return new BERConstructedOctetString(
                            buildDEREncodableVector(defIn).v);
                case SEQUENCE:
                    if (lazyEvaluate) {
                        return new LazyDERSequence(defIn.toByteArray());
                    } else {
                        return DERFactory
                                .createSequence(buildDEREncodableVector(defIn));
                    }
                case SET:
                    return DERFactory.createSet(buildDEREncodableVector(defIn),
                            false);
                case EXTERNAL:
                    return new DERExternal(buildDEREncodableVector(defIn));
                default:
                    return new DERUnknownTag(true, tagNo, defIn.toByteArray());
            }
        }

        return createPrimitiveDERObject(tagNo, defIn.toByteArray());
    }

    ASN1EncodableVector buildEncodableVector() throws IOException {
        ASN1EncodableVector v = new ASN1EncodableVector();
        DERObject o;

        while ((o = readObject()) != null) {
            v.add(o);
        }

        return v;
    }

    ASN1EncodableVector buildDEREncodableVector(DefiniteLengthInputStream dIn)
            throws IOException {
        return new ASN1InputStream(dIn).buildEncodableVector();
    }

    public DERObject readObject() throws IOException {
        int tag = read();
        if (tag <= 0) {
            if (tag == 0) {
                throw new IOException("unexpected end-of-contents marker");
            }

            return null;
        }

        //
        // calculate tag number
        //
        int tagNo = readTagNumber(this, tag);

        boolean isConstructed = (tag & CONSTRUCTED) != 0;

        //
        // calculate length
        //
        int length = readLength();

        if (length < 0) // indefinite length method
        {
            if (!isConstructed) {
                throw new IOException(
                        "indefinite length primitive encoding encountered");
            }

            IndefiniteLengthInputStream indIn = new IndefiniteLengthInputStream(
                    this, limit);
            ASN1StreamParser sp = new ASN1StreamParser(indIn, limit);

            if ((tag & APPLICATION) != 0) {
                return new BERApplicationSpecificParser(tagNo, sp)
                        .getLoadedObject();
            }

            if ((tag & TAGGED) != 0) {
                return new BERTaggedObjectParser(true, tagNo, sp)
                        .getLoadedObject();
            }

            // TODO There are other tags that may be constructed (e.g.
            // BIT_STRING)
            switch (tagNo) {
                case OCTET_STRING:
                    return new BEROctetStringParser(sp).getLoadedObject();
                case SEQUENCE:
                    return new BERSequenceParser(sp).getLoadedObject();
                case SET:
                    return new BERSetParser(sp).getLoadedObject();
                case EXTERNAL:
                    return new DERExternalParser(sp).getLoadedObject();
                default:
                    throw new IOException("unknown BER object encountered");
            }
        } else {
            try {
                return buildObject(tag, tagNo, length);
            } catch (IllegalArgumentException e) {
                throw new ASN1Exception("corrupted stream detected", e);
            }
        }
    }

    static int readTagNumber(InputStream s, int tag) throws IOException {
        int tagNo = tag & 0x1f;

        //
        // with tagged object tag number is bottom 5 bits, or stored at the
        // start of the content
        //
        if (tagNo == 0x1f) {
            tagNo = 0;

            int b = s.read();

            // X.690-0207 8.1.2.4.2
            // "c) bits 7 to 1 of the first subsequent octet shall not all be zero."
            if ((b & 0x7f) == 0) // Note: -1 will pass
            {
                throw new IOException(
                        "corrupted stream - invalid high tag number found");
            }

            while ((b >= 0) && ((b & 0x80) != 0)) {
                tagNo |= (b & 0x7f);
                tagNo <<= 7;
                b = s.read();
            }

            if (b < 0) {
                throw new EOFException("EOF found inside tag value.");
            }

            tagNo |= (b & 0x7f);
        }

        return tagNo;
    }

    static int readLength(InputStream s, int limit) throws IOException {
        int length = s.read();
        if (length < 0) {
            throw new EOFException("EOF found when length expected");
        }

        if (length == 0x80) {
            return -1; // indefinite-length encoding
        }

        if (length > 127) {
            int size = length & 0x7f;

            // Note: The invalid long form "0xff" (see X.690 8.1.3.5c) will be
            // caught here
            if (size > 4) {
                throw new IOException("DER length more than 4 bytes: " + size);
            }

            length = 0;
            for (int i = 0; i < size; i++) {
                int next = s.read();

                if (next < 0) {
                    throw new EOFException("EOF found reading length");
                }

                length = (length << 8) + next;
            }

            if (length < 0) {
                throw new IOException(
                        "corrupted stream - negative length found");
            }

            if (length >= limit) // after all we must have read at least 1 byte
            {
                throw new IOException(
                        "corrupted stream - out of bounds length found");
            }
        }

        return length;
    }

    static DERObject createPrimitiveDERObject(int tagNo, byte[] bytes) {
        switch (tagNo) {
            case BIT_STRING:
                return DERBitString.fromOctetString(bytes);
            case BMP_STRING:
                return new DERBMPString(bytes);
            case BOOLEAN:
                // BEGIN android-changed
                return DERBoolean.getInstance(bytes);
                // END android-changed
            case ENUMERATED:
                return new ASN1Enumerated(bytes);
            case GENERALIZED_TIME:
                return new ASN1GeneralizedTime(bytes);
            case GENERAL_STRING:
                return new DERGeneralString(bytes);
            case IA5_STRING:
                return new DERIA5String(bytes);
            case INTEGER:
                return new ASN1Integer(bytes);
            case NULL:
                return DERNull.INSTANCE; // actual content is ignored (enforce 0
                                         // length?)
            case NUMERIC_STRING:
                return new DERNumericString(bytes);
            case OBJECT_IDENTIFIER:
                return new ASN1ObjectIdentifier(bytes);
            case OCTET_STRING:
                return new DEROctetString(bytes);
            case PRINTABLE_STRING:
                return new DERPrintableString(bytes);
            case T61_STRING:
                return new DERT61String(bytes);
            case UNIVERSAL_STRING:
                return new DERUniversalString(bytes);
            case UTC_TIME:
                return new ASN1UTCTime(bytes);
            case UTF8_STRING:
                return new DERUTF8String(bytes);
            case VISIBLE_STRING:
                return new DERVisibleString(bytes);
            default:
                return new DERUnknownTag(false, tagNo, bytes);
        }
    }
}
